//
//  ComplexShapeFactory.cpp
//  Ray Tracer
//
//  Created by Kaila, Ashish on 4/14/13.
//  Copyright (c) 2013 Kaila, Ashish. All rights reserved.
//

#include "ModelFactory.h"
#include "CompositeShape.h"
#include "TriangleShape.h"
#include "../Materials/Material.h"
#include "../External/3ds.h"
#include <limits>
#include <fstream>
#include <sstream>

void updateDimensions(const Point3D& vector, double& left, double& right, double& top, double& bottom, double& close)
{
    left = fmin(vector[0], left);
    right = fmax(vector[0], right);
    top = fmax(vector[1], top);
    bottom = fmin(vector[1], bottom);
    close = fmax(vector[2], close);
}

CompositeShape* Load3DFile(const std::string& modelFilename, const Material* material)
{
    // Load the list of triangles from the .3ds
    C3DS sceneObj;
    if (!sceneObj.Create((char *)modelFilename.c_str()))
    {
        printf ("Error loading .3ds file\n");
        return NULL;
    }
    
    CompositeShape* shape = new CompositeShape();
    
    double left = std::numeric_limits<double>::max();
    double close = std::numeric_limits<double>::min();
    double bottom = std::numeric_limits<double>::max();
    double right = std::numeric_limits<double>::min();
    double top = std::numeric_limits<double>::min();
    
    for (unsigned int obj = 0; obj < (unsigned int)sceneObj.m_iNumMeshs; obj++)
    {
        for (unsigned int n = 0; n < (unsigned int)sceneObj.m_pMeshs[obj].iNumFaces; n++)
        {
            Point3D v1, v2, v3;
         
            // Vert 1
            v1[0] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[0]].x;
            v1[1] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[0]].y;
            v1[2] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[0]].z;
            updateDimensions(v1, left, right, top, bottom, close);
            
            // Vert 2
            v2[0] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[1]].x;
            v2[1] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[1]].y;
            v2[2] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[1]].z;
            updateDimensions(v2, left, right, top, bottom, close);
            
            // Vert 3
            v3[0] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[2]].x;
            v3[1] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[2]].y;
            v3[2] = sceneObj.m_pMeshs[obj].pVerts[sceneObj.m_pMeshs[obj].pFaces[n].corner[2]].z;
            updateDimensions(v3, left, right, top, bottom, close);
            
            // Note: Triangle shape takes in vertices in clockwise winding
            TriangleShape* triangle = new TriangleShape(v3, v2, v1, material);

            // Texture Coords
            if (sceneObj.m_pMeshs[obj].bTextCoords)
            {
                for (int i = 0; i < 3; i++)
                {
                    double u = sceneObj.m_pMeshs[obj].pTexs[sceneObj.m_pMeshs[obj].pFaces[n].corner[i]].tu;
                    double v = sceneObj.m_pMeshs[obj].pTexs[sceneObj.m_pMeshs[obj].pFaces[n].corner[i]].tv;
                    triangle->setTexturePoint(i, u, v);
                }
            }
            
            shape->addChildShape(triangle);
        }
    }

    shape->translate(Vector3D(-1 * (left + right) / 2.0, -1 * (top + bottom) / 2.0, close > 0 ? -1.5 * close : close / 2.0));
    
    sceneObj.Release();
    return shape;
}

CompositeShape* LoadObjFile(const std::string& modelFilename, const Material* material)
{
    std::ifstream ifs(modelFilename.c_str(), std::ifstream::in);
    
    if (!ifs.good())
    {
        printf ("Error loading .obj file\n");
        return false;
    }
    
    std::string line, key;
    
    vector<Point3D> vertices;
    vector<Vector3D> normals;
    vector<std::pair<double, double>> texcoords;
    CompositeShape* shape = new CompositeShape();
    
    double left = std::numeric_limits<double>::max();
    double close = std::numeric_limits<double>::min();
    double bottom = std::numeric_limits<double>::max();
    double right = std::numeric_limits<double>::min();
    double top = std::numeric_limits<double>::min();
    
    while (ifs.good() && !ifs.eof() && std::getline(ifs, line)) {
		key = "";
		std::stringstream stringstream(line);
		stringstream >> key >> std::ws;
		
		if (key == "v") { // vertex
			Point3D v; float x;
            int i = 0;
			while (!stringstream.eof()) {
				stringstream >> x >> std::ws;
                
                if (i < 3)
                {
                    v[i++] = x;
                }
			}
			vertices.push_back(v);
            updateDimensions(v, left, right, top, bottom, close);
		} else if (key == "vt") { // texture coordinate
			double u;
            double v;
            float x;
            int i = 0;
			while (!stringstream.eof()) {
				stringstream >> x >> std::ws;
				if (i++ == 0)
                {
                    u = x;
                }
                else
                {
                    v = x;
                }
			}
			texcoords.push_back(std::make_pair(u, v));
		} else if (key == "vn") { // normal
			Vector3D n; float x;
            int i = 0;
			while (!stringstream.eof()) {
				stringstream >> x >> std::ws;
				if (i < 3)
                {
                    n[i++] = x;
                }
			}
			n.normalize();
			normals.push_back(n);
		} else if (key == "f") { // face
			vector<int> vertexIndices;
            vector<int> normalIndices;
            vector<int> textureIndices;
            int v, t, n;
			while (!stringstream.eof()) {
				stringstream >> v >> std::ws;
				vertexIndices.push_back(v-1);
				if (stringstream.peek() == '/') {
					stringstream.get();
					if (stringstream.peek() == '/') {
						stringstream.get();
						stringstream >> n >> std::ws;
						normalIndices.push_back(n-1);
					} else {
						stringstream >> t >> std::ws;
						textureIndices.push_back(t-1);
						if (stringstream.peek() == '/') {
							stringstream.get();
							stringstream >> n >> std::ws;
							normalIndices.push_back(n-1);
						}
					}
				}
			}
			
            TriangleShape* triangle = new TriangleShape(vertices[vertexIndices[0]],
                                                        vertices[vertexIndices[1]],
                                                        vertices[vertexIndices[2]],
                                                        material);
            
            if (textureIndices.size() >= 3)
            {
                for (int i = 0; i < 3; i++)
                {
                    triangle->setTexturePoint(i, texcoords[textureIndices[i]].first, texcoords[textureIndices[i]].second);
                }
            }
            
            if (normalIndices.size() >= 3)
            {
                for (int i = 0; i < 3; i++)
                {
                    triangle->setNormals(i, normals[normalIndices[i]]);
                }
            }

            shape->addChildShape(triangle);
            
            // Break quad into 2 triangles
            if (textureIndices.size() == 4)
            {
                triangle = new TriangleShape(vertices[vertexIndices[2]],
                                             vertices[vertexIndices[3]],
                                             vertices[vertexIndices[0]],
                                             material);
            
                if (textureIndices.size() > 3)
                {
                    for (int i = 1; i < 4; i++)
                    {
                        triangle->setTexturePoint(i - 1, texcoords[textureIndices[(i + 1) % 4]].first, texcoords[textureIndices[(i + 1) % 4]].second);
                    }
                }
                
                if (normalIndices.size() > 3)
                {
                    for (int i = 1; i < 4; i++)
                    {
                        triangle->setNormals(i - 1, normals[normalIndices[(i + 1) % 4]]);
                    }
                }
                
                shape->addChildShape(triangle);                
            }
		}
	}
	ifs.close();
    shape->translate(Vector3D(-1 * (left + right) / 2.0, -1 * (top + bottom) / 2.0, close > 0 ? -1.5 * close : close / 2.0));
    return shape;
}

CompositeShape* ModelFactory::createCompositeShapeFromModel(const std::string& modelFilename, const Material* material)
{
    const std::string extn = modelFilename.substr(modelFilename.length() - 4, 4);
    
    if (extn == ".3ds")
    {
        return Load3DFile(modelFilename, material);
    }
    else if (extn == ".obj")
    {
        return LoadObjFile(modelFilename, material);
    }
    
    return NULL;
}
